/** biome-ignore-all lint/a11y/useKeyWithClickEvents: <explanation> */

import { i18n } from '@knno/i18n';
import time from '@knno/time';
import SVGLeft from '../../svg/chevron-left.svg';
import SVGRight from '../../svg/chevron-right.svg';
import { DATA } from '../symbol';
import { calendarStyle } from './calendar.css';
import '../../dict/dict';
import { Component, cls, type StyledProps } from '@knno/jsx';

export type DateValue = Date | string;
export type CalMode = 'year' | 'month' | 'date';

export interface CalDataBase {
	selected: Date[];
	isRange: boolean;
}

interface CalData extends CalDataBase {
	mode: CalMode;
	time: Date;
	keyboard: boolean;
	min?: Date | null;
	max?: Date | null;
}

export function dateValue(store: CalDataBase): Date[] | Date | null;
export function dateValue(store: CalDataBase, val: DateValue[] | DateValue | null): void;
export function dateValue(
	store: CalDataBase,
	val?: DateValue[] | DateValue | null,
): void | Date[] | Date | null {
	if (val === undefined) {
		if (store.isRange) {
			return store.selected;
		} else {
			return store.selected[0] ?? null;
		}
	}
	if (store.isRange) {
		if (val instanceof Array) {
			store.selected = val
				.slice(0, 2)
				.map((v) => (typeof v === 'string' ? time.parse(v) : v))
				.filter((v): v is Date => v !== null);
			if (store.selected.length === 1) {
				store.selected[1] = store.selected[0];
			}
		} else if (val instanceof Date) {
			store.selected = [val, val];
		} else if (typeof val === 'string') {
			const d = time.parse(val);
			store.selected = d === null ? [] : [d, d];
		} else {
			store.selected = [];
		}
	} else {
		if (val instanceof Array) {
			store.selected = val
				.slice(0, 1)
				.map((v) => (typeof v === 'string' ? time.parse(v) : v))
				.filter((v): v is Date => v !== null);
		} else if (val instanceof Date) {
			store.selected = [val];
		} else if (typeof val === 'string') {
			const d = time.parse(val);
			store.selected = d === null ? [] : [d];
		} else {
			store.selected = [];
		}
	}
	store.selected.sort((a, b) => a.getTime() - b.getTime());
}

export type CalendarProps = StyledProps<Calendar> & {
	disabled?: boolean;
	range?: boolean;
	value?: DateValue | DateValue[];
	min?: DateValue;
	max?: DateValue;
	mode?: CalMode;
	onPick?: (this: Calendar, value: Date[] | Date | null) => void;
};

export class Calendar extends Component<CalendarProps, Calendar, HTMLDivElement> {
	private [DATA]: CalData = {
		keyboard: false,
		selected: [],
		mode: 'date',
		isRange: false,
		time: new Date(),
	};

	private get body(): HTMLDivElement {
		return this.el.querySelector(':scope>div.body')! as HTMLDivElement;
	}

	private get titleDiv(): HTMLDivElement {
		return this.el.querySelector(':scope>div.bar>div.title')! as HTMLDivElement;
	}

	constructor(props: CalendarProps) {
		super(props);
		this[DATA].mode = props.mode ?? 'date';
		this[DATA].isRange = props.range ?? false;
		this[DATA].min = props.min ? time.parse(props.min) : null;
		this[DATA].max = props.max ? time.parse(props.max) : null;
		if (props.value) {
			this.setValue(props.value);
		}
		this.renderDate();
	}

	protected override render(): Node {
		let timer: number | null = null;
		const supportFlip = (elem: HTMLDivElement, direction: -1 | 1): HTMLDivElement => {
			elem.addEventListener('pointerup', (evt) => {
				(evt.target as HTMLElement).releasePointerCapture(evt.pointerId);
				if (timer !== null) clearTimeout(timer);
			});
			elem.addEventListener('pointerdown', (evt) => {
				this[DATA].keyboard = false;
				(evt.target as HTMLElement).setPointerCapture(evt.pointerId);
				const flip = () => {
					this.flip(direction);
					timer = setTimeout(flip, 100);
				};
				timer = setTimeout(flip, 500);
			});
			return elem;
		};
		return (
			<div
				class={cls(this.props.class, calendarStyle, this.props.disabled ? 'disabled' : '')}
				style={this.props.style}
				tabIndex={0}
				onKeyDown={(evt) => {
					const m = this[DATA].mode;
					if (evt.code === 'ArrowDown') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						if (m === 'year') {
							this.move(3, 'y');
						} else if (m === 'month') {
							this.move(3, 'M');
						} else {
							this.move(7, 'd');
						}
					} else if (evt.code === 'ArrowUp') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						if (m === 'year') {
							this.move(-3, 'y');
						} else if (m === 'month') {
							this.move(-3, 'M');
						} else {
							this.move(-7, 'd');
						}
					} else if (evt.code === 'ArrowLeft') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						if (m === 'year') {
							this.move(-1, 'y');
						} else if (m === 'month') {
							this.move(-1, 'M');
						} else {
							this.move(-1, 'd');
						}
					} else if (evt.code === 'ArrowRight') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						if (m === 'year') {
							this.move(1, 'y');
						} else if (m === 'month') {
							this.move(1, 'M');
						} else {
							this.move(1, 'd');
						}
					} else if (evt.code === 'PageUp') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						this.flip(-1);
					} else if (evt.code === 'PageDown') {
						evt.preventDefault();
						evt.stopPropagation();
						this[DATA].keyboard = true;
						this.flip(1);
					}
				}}
				onKeyUp={(evt) => {
					if (evt.code === 'Enter') {
						evt.preventDefault();
						evt.stopPropagation();
						if (this[DATA].keyboard) {
							this.select();
							this.renderDate();
							this.call('onPick', this.value);
						}
					}
				}}
			>
				<div class="bar">
					{supportFlip(
						(
							<div
								class="btn"
								innerHTML={SVGLeft}
								style={{ padding: '0 1em' }}
								onClick={() => {
									if (timer !== null) clearTimeout(timer);
									this.flip(-1);
								}}
							/>
						) as HTMLDivElement,
						-1,
					)}
					<div class="title" />
					{supportFlip(
						(
							<div
								class="btn"
								innerHTML={SVGRight}
								style={{ padding: '0 1em' }}
								onClick={() => {
									if (timer !== null) clearTimeout(timer);
									this.flip(1);
								}}
							/>
						) as HTMLDivElement,
						1,
					)}
				</div>
				<div class="body" />
			</div>
		);
	}

	private move(direction: number, unit: 'y' | 'M' | 'd') {
		this[DATA].time = time.add(this[DATA].time, direction, unit)!;
		this.adjust();
		this.renderDate();
	}

	private adjust() {
		const min = this.min;
		if (min && this[DATA].time < min) {
			this[DATA].time = new Date(min);
		}
		const max = this.max;
		if (max && this[DATA].time > max) {
			this[DATA].time = new Date(max);
		}
	}

	private flip(direction: 1 | -1) {
		const m = this[DATA].mode;
		if (m === 'year') {
			this[DATA].time = time.add(this[DATA].time, 10 * direction, 'y')!;
		} else if (m === 'month') {
			this[DATA].time = time.add(this[DATA].time, direction, 'y')!;
		} else {
			this[DATA].time = time.add(this[DATA].time, direction, 'M')!;
		}
		this.adjust();
		this.renderDate();
	}

	private isSelected(date: Date): boolean {
		if (this[DATA].selected.length === 0) return false;
		const arr = this[DATA].selected.slice(0, 2).map((d) => {
			if (this[DATA].mode === 'year') {
				return new Date(d.getFullYear(), 0, 1);
			} else if (this[DATA].mode === 'month') {
				return new Date(d.getFullYear(), d.getMonth(), 1);
			} else {
				return new Date(d.getFullYear(), d.getMonth(), d.getDate());
			}
		});
		if (arr.length === 1) arr.push(arr[0]);
		return date >= arr[0] && date <= arr[1];
	}

	private select() {
		if (this[DATA].isRange && this[DATA].selected.length === 1) {
			this[DATA].selected.push(new Date(this[DATA].time));
			this[DATA].selected.sort((a, b) => a.getTime() - b.getTime());
		} else {
			this[DATA].selected = [new Date(this[DATA].time)];
		}
	}

	private renderYear(body: HTMLDivElement) {
		const current = this[DATA].time.getFullYear();
		const year = current - (current % 10) - 1;
		const yearOfToday = new Date().getFullYear();
		this.titleDiv.textContent = `${year + 1} ~ ${year + 10}`;
		const minYear = this.min?.getFullYear();
		const maxYear = this.max?.getFullYear();
		body.setAttribute('mode', 'year');
		body.replaceChildren(
			...Array(12)
				.fill(0)
				.map((_, idx) => {
					const t = new Date(year + idx, 0, 1);
					const disabled =
						(minYear != null && year + idx < minYear) || (maxYear != null && year + idx > maxYear);
					return (
						<div
							class={[
								'c' + idx,
								year + idx === current ? 'active' : '',
								year + idx === yearOfToday ? 'today' : '',
								this.isSelected(t) ? 'selected' : '',
								disabled ? 'disabled' : '',
							]}
							disabled={disabled}
							textContent={`${year + idx}`}
							onClick={() => {
								this[DATA].keyboard = false;
								this[DATA].time = new Date(year + idx, 0, 1);
								this.select();
								this.renderDate();
								this.call('onPick', this.value);
							}}
						/>
					);
				}),
		);
	}

	private renderMonth(body: HTMLDivElement) {
		const year = this[DATA].time.getFullYear();
		this.titleDiv.textContent = year + '';
		const today = new Date();
		const yearOfToday = today.getFullYear();
		const monthOfToday = today.getMonth();
		const min = this.min;
		const max = this.max;
		const minTime = min ? new Date(min.getFullYear(), min.getMonth(), 1) : null;
		const maxTime = max ? new Date(max.getFullYear(), max.getMonth(), 1) : null;
		body.setAttribute('mode', 'month');
		body.replaceChildren(
			...Array(12)
				.fill(0)
				.map((_, idx) => {
					const t = new Date(year, idx, 1);
					const disabled = (minTime != null && t < minTime) || (maxTime != null && t > maxTime);
					return (
						<div
							class={[
								'c' + idx,
								year === this[DATA].time.getFullYear() && idx === this[DATA].time.getMonth()
									? 'active'
									: '',
								year === yearOfToday && idx === monthOfToday ? 'today' : '',
								this.isSelected(t) ? 'selected' : '',
								disabled ? 'disabled' : '',
							]}
							disabled={disabled}
							textContent={i18n(time.nameOfMonth(idx, true))}
							onClick={() => {
								this[DATA].keyboard = false;
								this[DATA].time = new Date(year, idx, 1);
								this.select();
								this.renderDate();
								this.call('onPick', this.value);
							}}
						/>
					);
				}),
		);
	}

	private renderDay(body: HTMLDivElement) {
		this.titleDiv.textContent = time.format(this[DATA].time, 'yyyy - M')!;
		const monthStart = new Date(this[DATA].time.getFullYear(), this[DATA].time.getMonth(), 1);
		const nextMonthStart = time.add(monthStart, 1, 'M')!;
		const begin = time.add(monthStart, -monthStart.getDay(), 'd')!;
		const today = new Date();
		const t = new Date(
			this[DATA].time.getFullYear(),
			this[DATA].time.getMonth(),
			this[DATA].time.getDate(),
		);
		const min = this.min;
		const max = this.max;
		const minTime = min ? new Date(min.getFullYear(), min.getMonth(), min.getDate()) : null;
		const maxTime = max ? new Date(max.getFullYear(), max.getMonth(), max.getDate()) : null;
		body.setAttribute('mode', 'day');
		body.replaceChildren(
			...Array(7)
				.fill(0)
				.map((_, idx) => {
					return <div class="week">{i18n(time.nameOfweek(idx, true).substring(0, 2))}</div>;
				}),
			...Array(42)
				.fill(0)
				.map((_, idx) => {
					const day = time.add(begin, idx, 'd')!;
					const disabled = (minTime != null && day < minTime) || (maxTime != null && day > maxTime);
					return (
						<div
							class={[
								day < monthStart || day >= nextMonthStart ? 'out' : '',
								day.getFullYear() === t.getFullYear() &&
								day.getMonth() === t.getMonth() &&
								day.getDate() === t.getDate()
									? 'active'
									: '',
								this.isSelected(day) ? 'selected' : '',
								day.getFullYear() === today.getFullYear() &&
								day.getMonth() === today.getMonth() &&
								day.getDate() === today.getDate()
									? 'today'
									: '',
								disabled ? 'disabled' : '',
							]}
							disabled={disabled}
							textContent={`${day.getDate()}`}
							onClick={() => {
								this[DATA].keyboard = false;
								t.setMonth(day.getMonth());
								t.setDate(day.getDate());
								this[DATA].time = t;
								this.select();
								this.renderDate();
								this.call('onPick', this.value);
							}}
						/>
					);
				}),
		);
	}

	private renderDate() {
		const body = this.body;
		const mode = this[DATA].mode;
		if (this[DATA].keyboard) {
			body.setAttribute('keyboard', '');
		} else {
			body.removeAttribute('keyboard');
		}
		if (mode === 'year') {
			this.renderYear(body);
		} else if (mode === 'month') {
			this.renderMonth(body);
		} else {
			this.renderDay(body);
		}
	}

	get mode(): CalMode {
		return this[DATA].mode;
	}
	setMode(value: CalMode): this {
		this[DATA].mode = value;
		this.renderDate();
		return this;
	}

	get min(): Date | null {
		return this[DATA].min ?? null;
	}
	setMin(value: Date | null): this {
		if (value) {
			this[DATA].min = new Date(value);
		} else delete this[DATA].min;
		this.renderDate();
		return this;
	}

	get max(): Date | null {
		return this[DATA].max ?? null;
	}
	setMax(value: Date | null): this {
		if (value) this[DATA].max = value;
		else delete this[DATA].max;
		this.renderDate();
		return this;
	}

	get value(): Date[] | Date | null {
		return dateValue(this[DATA]);
	}
	setValue(val: DateValue[] | DateValue | null): this {
		dateValue(this[DATA], val);
		if (this[DATA].selected.length > 0) {
			this[DATA].time = this[DATA].selected[0];
		}
		this.renderDate();
		return this;
	}

	get text(): string {
		if (this[DATA].selected.length === 0) return '';
		let pattern: string;
		if (this[DATA].mode === 'year') {
			pattern = 'yyyy';
		} else if (this[DATA].mode === 'month') {
			pattern = 'yyyy-MM';
		} else {
			pattern = 'yyyy-MM-dd';
		}
		const arr = this[DATA].selected.slice(0, 2);
		if (this[DATA].isRange && arr.length === 1) {
			arr.push(arr[0]);
		}
		return arr.map((d) => time.format(d, pattern)).join(' ~ ');
	}
	setText(value: string): this {
		return this.setValue(value || null);
	}

	get range(): boolean {
		return this[DATA].isRange;
	}
	setRange(value: boolean): this {
		this[DATA].isRange = value;
		this.setValue(this.value);
		return this;
	}

	get disabled(): boolean {
		return this.el.classList.contains('disabled');
	}
	setDisabled(value: boolean): this {
		this.el.classList.toggle('disabled', value);
		return this;
	}

	focus(): this {
		this.el.focus();
		return this;
	}
}
